<link rel="import" href="../polymer/polymer.html">

<dom-module id="vz-histogram-timeseries">

    <template>
      <style>
        :host {
          display: block;
        }

        svg {
          font-family: roboto, sans-serif;
        }

        .background {
          fill-opacity: 0;
          fill: red;
        }

        .histogram {
          pointer-events: none;
        }

        .hover {
          font-size: 9px;
          dominant-baseline: middle;
          opacity: 0;
        }

        .hover circle {
          stroke: white;
          stroke-opacity: 0.5;
          stroke-width: 1px;
        }

        .baseline {
          stroke: black;
          stroke-opacity: 0.1;
        }

        .outline {
          fill: none;
          stroke: white;
          stroke-opacity: 0.5;
        }

        .x-axis-hover {
          pointer-events: none;
        }

        .x-axis-hover .label {
          opacity: 0;
          font-weight: bold;
          font-size: 11px;
          text-anchor: end;
        }

        .x-axis-hover text {
          text-anchor: middle;
        }

        .x-axis-hover line {
          stroke: black;
        }

        .x-axis-hover rect {
          fill: white;
        }

        .gradient {
        }

        .axis {
          font-size: 10px;
          fill: #aaa;
        }

        .axis path.domain {
          fill: none;
        }

        .axis .tick line {
          stroke: #ddd;
        }

        .axis.slice {
          opacity: 0;
        }

        .axis.slice .tick line {
          stroke-dasharray: 2;
        }

        .small .axis text { display: none; }
        .small .axis .tick:first-of-type text { display: block; }
        .small .axis .tick:last-of-type text { display: block; }
        .medium .axis text { display: none; }
        .medium .axis .tick:nth-child(2n + 1) text { display: block; }
        .large .axis text { display: none; }
        .large .axis .tick:nth-child(2n + 1) text { display: block; }

      </style>
      <svg id="svg">
        <g>
          <g class="axis x"></g>
          <g class="axis y"></g>
          <g class="axis y slice"></g>
          <g class="stage">
            <rect class="background"></rect>
          </g>
          <g class="x-axis-hover"></g>
        </g>
      </svg>

    </template>

    <script>
    "use strict";
    Polymer({
      is: "vz-histogram-timeseries",
      properties: {
        mode: { type: String, value: "offset" }, //offset | overlay
        width: { type: Number, value: 500 },
        height: { type: Number, value: 500 },
        timeProperty: { type: String, value: "step" },
        bins: { type: String, value: "bins" },
        x: { type: String, value: "x" },
        dx: { type: String, value: "dx" },
        y: { type: String, value: "y" },
        data: { type: Array, value: function(){ return [{ step: 0, bins: [{ x: 0, dx: 1, y: 0 }] }, { step: 1, bins: [{ x: 0, dx: 1, y: 0 }] }];}}
        // type: HistogramSeriesDatum[] as described in vz-histogram-timeseries.d.ts
      },
      ready: function() {
        // Polymer's way of scoping styles on nodes that d3 created
        this.scopeSubtree(this.$["svg"], true);
      },
      draw: function(duration) {

        //
        // Data verification
        //
        if (!(this.data.length > 0)) throw(new Error("Not enough steps in the data"));
        if (!this.data[0].hasOwnProperty(this.timeProperty)) throw(new Error("No time property of '" + this.timeProperty + "' in data"));
        if (!this.data[0].hasOwnProperty(this.bins)) throw(new Error("No bins property of '" + this.bins + "' in data"));
        if (!(this.data[0][this.bins].length > 0)) throw(new Error("Must have at least one bin in bins in data"));
        if (!this.data[0][this.bins][0].hasOwnProperty(this.x)) throw(new Error("No x property '" + this.x + "' on bins data"));
        if (!this.data[0][this.bins][0].hasOwnProperty(this.dx)) throw(new Error("No dx property '" + this.dx + "' on bins data"));
        if (!this.data[0][this.bins][0].hasOwnProperty(this.y)) throw(new Error("No y property '" + this.y + "' on bins data"));

        //
        // Initialization
        //
        var timeProp = this.timeProperty;
        var xProp = this.x;
        var binsProp = this.bins;
        var dxProp = this.dx;
        var yProp = this.y;

        var xAccessor = (d) => d[xProp];
        var yAccessor = (d) => d[yProp];
        var dxAccessor = (d) => d[dxProp];
        var xRightAccessor = (d) => d[xProp] + d[dxProp];
        var timeAccessor = (d) => d[timeProp];

        var duration = duration | 0;
        var data = this.data;
        var mode = this.mode;

        var outerWidth = this.width,
            outerHeight = this.height;

        var sliceHeight,
            margin = {top: 5, right: 60, bottom: 20, left: 24};

        if (mode === "offset") {
          sliceHeight = outerHeight / 2.5;
          margin.top = sliceHeight + 5;
        } else {
          sliceHeight = outerHeight - margin.top - margin.bottom;
        }

        var width = outerWidth - margin.left - margin.right,
            height = outerHeight - margin.top - margin.bottom;

        var leftMin = d3.min(data, xAccessor),
            rightMax = d3.max(data, xRightAccessor);

        //
        // Text formatters
        //
        var formatTime = d3.time.format("%x"),
            format = d3.format(".3n");

        //
        // Calculate the extents
        //
        var xExtents = data.map(function(d, i) {
          return [
            d3.min(d[binsProp], xAccessor),
            d3.max(d[binsProp], xRightAccessor)
          ];
        });
        var yExtents = data.map(function(d) {
          return d3.extent(d[binsProp], yAccessor);
        });

        //
        // Scales and axis
        //
        var outlineCanvasSize = 500;

        var yScale = (timeProp === "step" ? d3.scale.linear() : d3.time.scale())
            .domain(d3.extent(data, timeAccessor))
            .range([0, (mode === "offset" ? height : 0)]);

        var ySliceScale = d3.scale.linear()
            .domain([0, d3.max(data, function(d, i) { return yExtents[i][1]; })])
            .range([sliceHeight, 0]);

        var yLineScale = d3.scale.linear()
            .domain(ySliceScale.domain())
            .range([outlineCanvasSize, 0]);

        var xScale = d3.scale.linear()
            .domain([
              d3.min(data, function(d, i) { return xExtents[i][0]; }),
              d3.max(data, function(d, i) { return xExtents[i][1]; })
            ])
            .nice()
            .range([0, width]);

        var xLineScale = d3.scale.linear()
            .domain(xScale.domain())
            .range([0, outlineCanvasSize]);

        var outlineColor = d3.scale.linear()
            .domain(d3.extent(data, timeAccessor))
            .range(["#FFA726", "#BF360C"])
            .interpolate(d3.interpolateHcl);

        var xAxis = d3.svg.axis()
            .scale(xScale)
            .ticks(Math.max(2, width / 20))
            .orient("bottom");

        var yAxis = d3.svg.axis()
            .scale(yScale)
            .ticks(Math.max(2, width / 20))
            .orient("right");

        var ySliceAxis = d3.svg.axis()
            .scale(ySliceScale)
            .ticks(Math.max(2, width / 20))
            .tickSize(width + 5)
            .orient("right");

        var path = d3.svg.area()
            .interpolate("linear")
            .x(function(d) { return xLineScale(d[xProp] + d[dxProp] / 2); })
            .y0(function(d) { return yLineScale(0); })
            .y1(function(d) { return yLineScale(d[yProp]); });

        //
        // Render
        //
        var svgNode = this.$.svg;

        var svg = d3.select(svgNode)

        var svgTransition = svg.transition().duration(duration)
            .attr("width", outerWidth)
            .attr("height", outerHeight);

        var g = svg.select("g")
            .classed("small", function() { return (width > 0 && width <= 150); })
            .classed("medium", function() { return (width > 150 && width <= 300); })
            .classed("large", function() { return (width > 300); })

        var gTransition = svgTransition.select("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        var bisect = d3.bisector(xRightAccessor).left;
        var stage = g.select(".stage")
            .on("mouseover", function() {
              hoverUpdate.transition().style("opacity", 1);
              edgeLabelUpdate.transition().style("opacity", 1)
            })
            .on("mouseout", function() {
              hoverUpdate.transition().style("opacity", 0);
              edgeLabelUpdate.transition().style("opacity", 0)
            })
            .on("mousemove", function() {
              var m = d3.mouse(this),
                  v = xScale.invert(m[0]);

              function hoverXIndex(d) {
                return Math.min(d[binsProp].length - 1, bisect(d[binsProp], v));
              }

              var lastSliceData;
              hoverUpdate
                .attr("transform", function(d) {
                  var index = hoverXIndex(d);
                  lastSliceData = d;
                  return "translate(" + xScale(d[binsProp][index][xProp] + d[binsProp][index][dxProp] / 2) + "," + ySliceScale(d[binsProp][index][yProp]) + ")";
                });

              var index = hoverXIndex(lastSliceData);

              edgeLabelUpdate
                  .attr("transform", function(d) { return"translate(" + xScale(lastSliceData[binsProp][index][xProp] + lastSliceData[binsProp][index][dxProp] / 2) + ", " + height + ")"; })
                .select("text")
                  .text(function(d) { return format(lastSliceData[binsProp][index][xProp] + lastSliceData[binsProp][index][dxProp] / 2); });
            });

        var background = stage.select(".background")
            .attr("transform", "translate(" + -margin.left + "," + -margin.top + ")")
            .attr("width", outerWidth)
            .attr("height", outerHeight);

        var histogram = stage.selectAll(".histogram").data(data, function(d) { return d[timeProp]; }),
            histogramExit = histogram.exit().remove(),
            histogramEnter = histogram.enter().append("g").attr("class", "histogram"),
            histogramUpdate = histogram
                .sort(function(a, b) { return a[timeProp] - b[timeProp]; }),
            histogramTransition = gTransition.selectAll(".histogram")
                .attr("transform", function(d) {
                  return "translate(0, " + (mode === "offset" ? (yScale(d[timeProp]) - sliceHeight) : 0) + ")";
                });

        var baselineEnter = histogramEnter.append("line").attr("class", "baseline"),
            baselineUpdate = histogramTransition.select(".baseline")
                .style("stroke-opacity", function(d) { return (mode === "offset" ? 0.1 : 0); })
                .attr("y1", sliceHeight)
                .attr("y2", sliceHeight)
                .attr("x2", width);

        var outlineEnter = histogramEnter.append("path").attr("class", "outline"),
            outlineUpdate = histogramUpdate.select(".outline")
                .attr("vector-effect", "non-scaling-stroke")
                .attr("d", function(d) { return path(d[binsProp]); })
                .style("stroke-width", 1),
            outlineTransition = histogramTransition.select(".outline")
                .attr("transform", "scale(" + width / outlineCanvasSize + ", " + sliceHeight / outlineCanvasSize + ")")
                .style("stroke", function(d) { return (mode === "offset" ? "white" : outlineColor(d[timeProp])); })
                .style("fill-opacity", function(d) { return (mode === "offset" ? 1 : 0); })
                .style("fill", function(d) { return outlineColor(d[timeProp]); });


        var hoverEnter = histogramEnter.append("g")
                .attr("class", "hover")
                .style("fill", function(d) { return outlineColor(d[timeProp]); }),
            hoverUpdate = histogramUpdate.select(".hover");

        hoverEnter.append("circle")
            .attr("r", 2);

        hoverEnter.append("text")
            .style("display", "none")
            .style("stroke", "white")
            .style("stroke-width", 2)
            .attr("dx", 4);

        hoverEnter.append("text")
            .style("display", "none")
            .attr("dx", 4);

        var edgeLabel = g.select(".x-axis-hover").selectAll(".label").data(["x"]),
            edgeLabelEnter = edgeLabel.enter().append("g").attr("class", "label"),
            edgeLabelUpdate = edgeLabel;

        edgeLabelEnter.append("rect")
            .attr("x", -20)
            .attr("y", 6)
            .attr("width", 40)
            .attr("height", 14)

        edgeLabelEnter.append("line")
            .attr("x1", 0)
            .attr("x2", 0)
            .attr("y1", 0)
            .attr("y2", 6);

        edgeLabelEnter.append("text")
            .attr("dy", 18);

        gTransition.select(".y.axis.slice")
            .style("opacity", mode === "offset" ? 0 : 1)
            .attr("transform", "translate(0, " + (mode === "offset" ? -sliceHeight : 0) + ")")
            .call(ySliceAxis);

        gTransition.select(".x.axis")
            .attr("transform", "translate(0, " + height + ")")
            .call(xAxis);

        gTransition.select(".y.axis")
            .style("opacity", mode === "offset" ? 1 : 0)
            .attr("transform", "translate(" + width + ", " + (mode === "offset" ? 0 : height) + ")")
            .call(yAxis);

      }
    });
    </script>

  </dom-module>
